#include "ethernet.h"
#include "ip.h"
#include "arp.h"
#include "icmp.h"
#include "udp.h"
#include <string.h>
#include <stdio.h>

static uint8_t src_ip[NET_IP_LEN] = DRIVER_IF_IP;
uint16_t global_id = 0;

/**
 * @brief 处理一个收到的数据包
 *        你首先需要做报头检查，检查项包括：版本号、总长度、首部长度等。
 * 
 *        接着，计算头部校验和，注意：需要先把头部校验和字段缓存起来，再将校验和字段清零，
 *        调用checksum16()函数计算头部检验和，比较计算的结果与之前缓存的校验和是否一致，
 *        如果不一致，则不处理该数据报。
 * 
 *        检查收到的数据包的目的IP地址是否为本机的IP地址，只处理目的IP为本机的数据报。
 * 
 *        检查IP报头的协议字段：
 *        如果是ICMP协议，则去掉IP头部，发送给ICMP协议层处理
 *        如果是UDP协议，则去掉IP头部，发送给UDP协议层处理
 *        如果是本实验中不支持的其他协议，则需要调用icmp_unreachable()函数回送一个ICMP协议不可达的报文。
 *          
 * @param buf 要处理的包
 */
void ip_in(buf_t *buf)
{
    // printf("ip: ip_in\n");
    // Check correction 
    ip_hdr_t* head = buf->data;
    if(!(head->version == IP_VERSION_4 &&
        swap16(head->total_len) == buf->len &&
        head->hdr_len >= 5)){
            printf("ip head broken!\n");
            icmp_unreachable(buf,head->src_ip,ICMP_CODE_PROTOCOL_UNREACH);
            return;
        }
    // Check sum
    uint16_t sum = head->hdr_checksum;
    head->hdr_checksum = 0;
    if(swap16(sum) != swap16(checksum16(head,(head->hdr_len)*4))){
        printf("check sum:%04x, cal check sum:%04x\n",sum,checksum16(buf,(head->hdr_len)*4));
        printf("checksum error!\n");
        return;
    }
    head->hdr_checksum = sum;
    // Ip match
    if(cmpn(head->dest_ip,src_ip,NET_IP_LEN)) 
        return;
    
    switch (head->protocol)
    {
    case NET_PROTOCOL_ICMP:
        buf_remove_header(buf,(head->hdr_len)*4);
        // printf("ip: icmp_in\n");
        icmp_in(buf,head->src_ip);
        break;
    // case NET_PROTOCOL_TCP:
    //     tcp_in(buf);
    //     break;
    case NET_PROTOCOL_UDP:
        buf_remove_header(buf,(head->hdr_len)*4);
        // printf("ip: udp_in\n");
        udp_in(buf,head->src_ip);
        break;
    default:
        printf("ip: icmp_unreachable\n");
        icmp_unreachable(buf,head->src_ip,ICMP_CODE_PROTOCOL_UNREACH);
        break;
    }
}

/**
 * @brief 处理一个要发送的ip分片
 *        你需要调用buf_add_header增加IP数据报头部缓存空间。
 *        填写IP数据报头部字段。
 *        将checksum字段填0，再调用checksum16()函数计算校验和，并将计算后的结果填写到checksum字段中。
 *        将封装后的IP数据报发送到arp层。
 * 
 * @param buf 要发送的分片
 * @param ip 目标ip地址
 * @param protocol 上层协议
 * @param id 数据包id
 * @param offset 分片offset，必须被8整除
 * @param mf 分片mf标志，是否有下一个分片
 */
void ip_fragment_out(buf_t *buf, uint8_t *ip, net_protocol_t protocol, int id, uint16_t offset, int mf)
{
    buf_add_header(buf,sizeof(ip_hdr_t));
    ip_hdr_t* head = buf->data;
    head->hdr_len = 5;
    head->version = IP_VERSION_4;
    head->tos = 0;
    head->total_len = swap16(buf->len);
    head->id = swap16(id);
    head->flags_fragment = ((mf*IP_MORE_FRAGMENT)+((offset&0b1111100000000)>>8)) + ((offset&0xff)<<8);
    head->ttl = 64;
    head->protocol = protocol;
    head->hdr_checksum = 0;
    memcpy(head->src_ip, src_ip, NET_IP_LEN);
    memcpy(head->dest_ip, ip, NET_IP_LEN);
    head->hdr_checksum = checksum16(head,sizeof(ip_hdr_t));
    arp_out(buf,ip,NET_PROTOCOL_IP);
}

/**
 * @brief 处理一个要发送的ip数据包
 *        你首先需要检查需要发送的IP数据报是否大于以太网帧的最大包长（1500字节 - 以太网报头长度）。
 *        
 *        如果超过，则需要分片发送。 
 *        分片步骤：
 *        （1）调用buf_init()函数初始化buf，长度为以太网帧的最大包长（1500字节 - 以太网报头长度）
 *        （2）将数据报截断，每个截断后的包长度 = 以太网帧的最大包长，调用ip_fragment_out()函数发送出去
 *        （3）如果截断后最后的一个分片小于或等于以太网帧的最大包长，
 *             调用buf_init()函数初始化buf，长度为该分片大小，再调用ip_fragment_out()函数发送出去
 *             注意：id为IP数据报的分片标识，从0开始编号，每增加一个分片，自加1。最后一个分片的MF = 0
 *    
 *        如果没有超过以太网帧的最大包长，则直接调用调用ip_fragment_out()函数发送出去。
 * 
 * @param buf 要处理的包
 * @param ip 目标ip地址
 * @param protocol 上层协议
 */
void ip_out(buf_t *buf, uint8_t *ip, net_protocol_t protocol)
{
    static int max_fragment_size =  ETHERNET_MTU-sizeof(ip_hdr_t);
    if(buf->len <= max_fragment_size){
        ip_fragment_out(buf,ip,protocol,global_id++,0,0);
        return;
    }
    
    // Split into ip fragment
    int offset = 0;
    printf("Split into ip fragment(len:%d)\n",buf->len);
    for(int i=buf->len; i> max_fragment_size; i-= max_fragment_size ){
        buf_t tmpbuf;
        buf_init(&tmpbuf, max_fragment_size);
        for(int j=0; j<max_fragment_size; j++)
            tmpbuf.data[j] = buf->data[j];
        buf_remove_header(buf,max_fragment_size);
        ip_fragment_out(&tmpbuf,ip,protocol,global_id,offset,1);
        offset += (max_fragment_size)/8;
    }
    buf_t tmpbuf;
    buf_init(&tmpbuf, buf->len);
    for(int j=0; j<buf->len; j++)
        tmpbuf.data[j] = buf->data[j];
    buf_remove_header(buf,buf->len);
    ip_fragment_out(&tmpbuf,ip,protocol,global_id++,offset,0);
}
